package org.codefilarete.stalactite.sql.result;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.codefilarete.reflection.MethodReferenceCapturer;
import org.codefilarete.stalactite.sql.result.ResultSetRowTransformer.BeanFactory;
import org.codefilarete.stalactite.sql.result.ResultSetRowTransformer.IdentifierArgBeanFactory;
import org.codefilarete.stalactite.sql.result.ResultSetRowTransformer.NoIdentifierBeanFactory;
import org.codefilarete.stalactite.sql.statement.binder.ResultSetReader;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.ThreadLocals;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.danekja.java.util.function.serializable.SerializableFunction;
import org.danekja.java.util.function.serializable.SerializableSupplier;

import static org.codefilarete.stalactite.sql.result.WholeResultSetTransformer.AssemblyPolicy.ON_EACH_ROW;

/**
 * A class aimed at transforming a whole {@link ResultSet} into a graph of objects.
 * Graph creation is declared through {@link #add(String, ResultSetReader, Class, SerializableFunction, BeanRelationFixer)}, be aware that relations will be
 * unstacked/applied in order of declaration.
 * Instances of this class can be reused over multiple {@link ResultSet} (supposed to have same columns) and are thread-safe for iteration.
 * They can also be adapted to other {@link ResultSet}s that haven't the exact same column names by duplicating them with {@link #copyWithAliases(Function)}.
 * Moreover, they can also be cloned to another type of bean which uses the same column names with {@link #copyFor(Class, SerializableFunction)}.
 * 
 * @param <C> assembled bean type
 * @param <I> bean identifier type
 * @author Guillaume Mary
 * @see #add(String, ResultSetReader, BiConsumer)
 * @see #add(String, ResultSetReader, Class, SerializableFunction, BeanRelationFixer)
 * @see #transformAll(ResultSet) 
 */
public class WholeResultSetTransformer<C, I> implements ResultSetTransformer<C, I>, CopiableForAnotherQuery<C> {
	
	/**
	 * Per-Thread caches for created beans during {@link ResultSet} iteration.
	 * Created as a {@link ThreadLocal} to share it between all {@link WholeResultSetTransformer}s that are implied in the bean graph creation :
	 * first approach was to use an instance variable and initialize it on all instances before {@link ResultSet} iteration and release it after,
	 * but this design had several drawbacks:
	 * - non-thread-safe usage of instances (implying synchronization during whole iteration !)
	 * - instances piped with {@link #add(BeanRelationFixer, ResultSetRowTransformer)} were impossible to wire to the bean cache without cloning them to a
	 * {@link WholeResultSetTransformer}
	 */
	static final ThreadLocal<SimpleBeanCache> CURRENT_BEAN_CACHE = new ThreadLocal<>();
	
	static final ThreadLocal<Set<TreatedRelation>> CURRENT_TREATED_ASSEMBLERS = ThreadLocal.withInitial(HashSet::new);
	
	private final RootConverter<C, I> rootConverter;
	
	/** The list of relations that will assemble objects */
	private final KeepOrderSet<Assembler<C>> assemblers = new KeepOrderSet<>();
	
	/** Raw consumers, not linked to any beans, will be run on each row, if someone expects it to be run once per bean, then attach it to root converter */
	private final Set<ColumnConsumer<C, Object>> consumers = new HashSet<>();
	
	/**
	 * Constructor with root bean instantiation parameters
	 * 
	 * @param rootType main bean type
	 * @param beanKeyColumnName the name of the column that contains bean key
	 * @param reader object to ease column reading, indicates column type
	 * @param beanFactory the bean creator, bean key will be passed as argument. Not called if bean key is null (no instantiation needed)
	 */
	public WholeResultSetTransformer(Class<C> rootType, String beanKeyColumnName, ResultSetReader<I> reader, SerializableFunction<I, C> beanFactory) {
		this(new ResultSetRowTransformer<>(rootType, beanKeyColumnName, reader, beanFactory));
	}
	
	/**
	 * Constructor with root bean instantiation parameters as a default Java Bean constructor and setter for key value
	 *
	 * @param rootType main bean type
	 * @param beanKeyColumnName the name of the column that contains bean key
	 * @param reader object to ease column reading, indicates column type
	 * @param beanFactory the bean constructor. Not called if bean key is null (no instantiation needed)
	 * @param setter setter for bean key
	 */
	public WholeResultSetTransformer(Class<C> rootType, String beanKeyColumnName, ResultSetReader<I> reader, Supplier<C> beanFactory, BiConsumer<C, I> setter) {
		this(rootType, beanKeyColumnName, reader, i -> {
			C newInstance = beanFactory.get();
			setter.accept(newInstance, i);
			return newInstance;
		});
	}
	
	/**
	 * Constructor with root bean instantiation parameters.
	 * 
	 * With this constructor a root instance per {@link ResultSet} row will be created since there's no mean to
	 * distinguish a row instance from another because no key reader is given. This is expected to be used for flat data retrieval. 
	 *
	 * @param rootType main bean type
	 * @param beanFactory the bean creator
	 */
	public WholeResultSetTransformer(Class<C> rootType, SerializableSupplier<C> beanFactory) {
		this(new ResultSetRowTransformer<>(rootType, new NoIdentifierBeanFactory<>(beanFactory)));
	}
	
	/**
	 * Special constructor aimed at defining root transformer when other constructors are insufficient
	 * 
	 * @param rootTransformer transformer that will create graph root-beans from {@link ResultSet}
	 */
	public WholeResultSetTransformer(ResultSetRowTransformer<C, I> rootTransformer) {
		this.rootConverter = new CachingResultSetRowTransformer<>(rootTransformer);
	}
	
	/**
	 * Defines a complementary column that will be mapped on a bean property.
	 * Null values will be passed to the consumer, hence the property mapper must be "null-value proof".
	 * Those consumers will be run on each row, if this behavior is not expected then one can attach it to root converter. 
	 * 
	 * @param columnConsumer the object that will do the reading and mapping
	 */
	@Override
	public <O> WholeResultSetTransformer<C, I> add(ColumnConsumer<C, O> columnConsumer) {
		consumers.add((ColumnConsumer<C, Object>) columnConsumer);
		return this;
	}
	
	/**
	 * Adds a bean relation to the main/root object.
	 * 
	 * @param columnName the column name to read the bean key
	 * @param reader a reader for reading the bean key
	 * @param beanType the type of the new bean
	 * @param beanFactory a factory for creating the new bean, not called if bean key is null
	 * @param combiner a callback that will combine the newly created bean and the one of this transformer, called even with a null bean (v)
	 * @param <K> ResultSet column type
	 * @param <V> new bean type
	 * 
	 * @return this
	 * @see #add(String, ResultSetReader, Class, BeanRelationFixer) 
	 */
	public <K, V> WholeResultSetTransformer<C, I> add(String columnName, ResultSetReader<K> reader, Class<V> beanType,
													  SerializableFunction<K, V> beanFactory, BeanRelationFixer<C, V> combiner) {
		ResultSetRowTransformer<V, K> relatedBeanCreator = new ResultSetRowTransformer<>(beanType, columnName, reader, beanFactory);
		add(combiner, relatedBeanCreator);
		return this;
	}
	
	/**
	 * Adds a bean relation to the main/root object.
	 * It is a simplified version of {@link #add(String, ResultSetReader, Class, SerializableFunction, BeanRelationFixer)} where the factory is the
	 * default constructor of the given type.
	 * Be aware that the factory doesn't take the column (bean key) value as a parameter, if no default constructor exists please prefer
	 * {@link #add(String, ResultSetReader, Class, SerializableFunction, BeanRelationFixer)} or {@link #add(String, ResultSetReader, BiConsumer)}
	 * 
	 * @param columnName the column name to read the bean key
	 * @param reader a reader for reading the bean key
	 * @param beanType the type of the new bean
	 * @param combiner a callback that will combine the newly created bean and the one of this transformer, called even with a null bean (v)
	 * @param <K> ResultSet column type
	 * @param <V> new bean type
	 * 
	 * @return this
	 */
	public <K, V> WholeResultSetTransformer<C, I> add(String columnName, ResultSetReader<K> reader, Class<V> beanType, BeanRelationFixer<C, V> combiner) {
		add(columnName, reader, beanType, v -> Reflections.newInstance(beanType), combiner);
		return this;
	}
	
	/**
	 * Combines this transformer with another one through a bean relation, creating a bean graph.
	 * Be aware that a copy of relatedBeanCreator is made to make it uses cache of beans during {@link ResultSet} iteration.
	 * Hence, modifying relatedBeanCreator after won't affect this.
	 * 
	 * @param <K> the type of the other bean keys
	 * @param <V> the type of the other beans
	 * @param combiner the wire between instances of this transformer and those of the given one
	 * @param relatedBeanCreator the manager of the other beans
	 * @return this
	 */
	@Override
	public <K, V> WholeResultSetTransformer<C, I> add(BeanRelationFixer<C, V> combiner, ResultSetRowTransformer<V, K> relatedBeanCreator) {
		return add(combiner, relatedBeanCreator, ON_EACH_ROW);
	}
	
	/**
	 * Combines this transformer with another one through a bean relation, hence a bean graph is created.
	 * Be aware that a copy of relatedBeanCreator is made to make it uses cache of beans during {@link ResultSet} iteration.
	 * Hence, modifying relatedBeanCreator after won't affect this.
	 *
	 * @param <K> the type of the other bean keys
	 * @param <V> the type of the other beans
	 * @param combiner the wire between instances of this transformer and those of the given one
	 * @param relatedBeanCreator the manager of the other beans
	 * @return this
	 */
	public <K, V> WholeResultSetTransformer<C, I> add(BeanRelationFixer<C, V> combiner, ResultSetRowTransformer<V, K> relatedBeanCreator, AssemblyPolicy assemblyPolicy) {
		CachingResultSetRowTransformer<V, K> relatedBeanCreatorCopy = new CachingResultSetRowTransformer<>(relatedBeanCreator);
		return add(new Relation<>(combiner, relatedBeanCreatorCopy), assemblyPolicy);
	}
	
	/**
	 * Adds a very generic way to assemble {@link ResultSet} rows to a root bean.
	 * Be aware that any bean created by given assembler won't participate in current instance cache, if this is required then one should implement
	 * its own cache.
	 * Assembly will occur on each row ({@link ResultSetRowAssembler#assemble(Object, ResultSet)} will be call for each {@link ResultSet} row)
	 * 
	 * @param assembler a generic combiner of a root bean and each {@link ResultSet} row 
	 * @return this
	 * @see #add(ResultSetRowAssembler, AssemblyPolicy)
	 */
	public WholeResultSetTransformer<C, I> add(ResultSetRowAssembler<C> assembler) {
		return add(assembler, ON_EACH_ROW);
	}
	
	/**
	 * Adds a very generic way to assemble {@link ResultSet} rows to a root bean.
	 * Be aware that any bean created by given assembler won't participate in current instance cache, if this is required then one should implement
	 * its own cache.
	 *
	 * @param assembler a generic combiner of a root bean and each {@link ResultSet} row
	 * @param assemblyPolicy policy to decide if given assemble shall be invoked on each row or not
	 * @return this
	 */
	public WholeResultSetTransformer<C, I> add(ResultSetRowAssembler<C> assembler, AssemblyPolicy assemblyPolicy) {
		this.assemblers.add(new Assembler<>(assembler, assemblyPolicy));
		return this;
	}
	
	@Override
	public <T extends C> WholeResultSetTransformer<T, I> copyFor(Class<T> beanType, SerializableFunction<I, T> beanFactory) {
		ResultSetRowTransformer<T, I> newRootConverter = this.rootConverter.copyFor(beanType, beanFactory);
		return copy(newRootConverter);
	}
	
	@Override
	public <T extends C> WholeResultSetTransformer<T, I> copyFor(Class<T> beanType, SerializableSupplier<T> beanFactory) {
		ResultSetRowTransformer<T, I> newRootConverter = this.rootConverter.copyFor(beanType, beanFactory);
		return copy(newRootConverter);
	}
	
	private <T extends C> WholeResultSetTransformer<T, I> copy(ResultSetRowTransformer<T, I> newRootConverter) {
		// Making the copy
		WholeResultSetTransformer<T, I> result = new WholeResultSetTransformer<>(newRootConverter);
		// Note: combiners are shared, not copied 
		result.assemblers.addAll((Collection) this.assemblers);
		result.consumers.addAll((Collection) this.consumers);
		return result;
	}
	
	@Override
	public WholeResultSetTransformer<C, I> copyWithAliases(Function<String, String> columnMapping) {
		// NB: rootConverter can be cloned without a cache checking bean factory because it already has it due to previous assignments
		// (follow rootConverter assignments to be sure)
		ResultSetRowTransformer<C, I> rootConverterCopy = this.rootConverter.copyWithAliases(columnMapping);
		WholeResultSetTransformer<C, I> result = new WholeResultSetTransformer<>(rootConverterCopy);
		this.assemblers.forEach(assembler ->
				result.add(assembler.getResultSetRowAssembler().copyWithAliases(columnMapping), assembler.getPolicy())
		);
		this.consumers.forEach(assembler ->
				result.add(assembler.copyWithAliases(columnMapping))
		);
		return result;
	}
	
	@Override	// for adhoc return type
	public WholeResultSetTransformer<C, I> copyWithAliases(Map<String, String> columnMapping) {
		return copyWithAliases(columnMapping::get);
	}
	
	public Set<C> transformAll(ResultSet resultSet) {
		return transformAll(resultSet, Accumulators.toCollection(LinkedHashSet::new));
	}
	
	public <R, S> R transformAll(ResultSet resultSet, Accumulator<C, S, R> accumulator) {
		// We convert the ResultSet with an iteration over a ResultSetIterator that uses the transform(ResultSet) method
		ResultSetIterator<C> resultSetIterator = new ResultSetIterator<C>(resultSet) {
			@Override
			public C convert(ResultSet resultSet) throws SQLException {
				return transform(resultSet);
			}
		};
		return doWithBeanCache(() -> accumulator.collect(() -> resultSetIterator));
	}
	
	/**
	 * Executes given code by initializing internal caches so one can use {@link #transform(ResultSet)} without getting {@link NullPointerException}
	 * due to their absence. Hence, given code is expected to use {@link #transform(ResultSet)} else this method is useless.
	 * This method is not to be considered as a primary usage of this class since its purpose is to iterate over a whole {@link ResultSet}.
	 * 
	 * @param callable any code using {@link #transform(ResultSet)}
	 * @param <O> type of instance returned by this method which is the one returned by given code
	 * @return object returned by given {@link Supplier}
	 */
	public <O> O doWithBeanCache(Supplier<O> callable) {
		return ThreadLocals.doWithThreadLocal(CURRENT_BEAN_CACHE, SimpleBeanCache::new,
				(Supplier<O>) () -> ThreadLocals.doWithThreadLocal(CURRENT_TREATED_ASSEMBLERS, HashSet::new, callable));
	}
	
	/**
	 * <strong>This method is not expected to be called from outside but is public to respect interface implementation</strong>
	 * Note that it uses an internal {@link ThreadLocal} to ensure {@link AssemblyPolicy#ONCE_PER_BEAN}
	 * 
	 * @param resultSet not null
	 * @return current row root bean
	 * @throws SQLException if an error occurs while reading given {@link ResultSet}
	 */
	@Override
	public C transform(ResultSet resultSet) throws SQLException {
		// Can it be possible to have a null root bean ? if such we should add a if-null prevention. But what's the case ?
		C currentRowBean = rootConverter.transform(resultSet);
		// Not made with stream because it doesn't handle well checked Exception
		for (Assembler<C> entry : this.assemblers) {
			ResultSetRowAssembler<C> rowAssembler = entry.getResultSetRowAssembler();
			switch (entry.getPolicy()) {
				case ON_EACH_ROW:
					rowAssembler.assemble(currentRowBean, resultSet);
					break;
				case ONCE_PER_BEAN:
					// we check if relation has already been treated for current bean : the key to be checked is a TreatedRelation
					TreatedRelation<C> treatedRelation = new TreatedRelation<>(currentRowBean, rowAssembler);
					Set<TreatedRelation> treatedRelations = CURRENT_TREATED_ASSEMBLERS.get();
					if (!treatedRelations.contains(treatedRelation)) {
						rowAssembler.assemble(currentRowBean, resultSet);
						treatedRelations.add(treatedRelation);
					}
					break;
			}
		}
		this.consumers.forEach(c -> c.assemble(currentRowBean, resultSet));
		return currentRowBean;
	}
	
	/**
	 * A relation between a property mutator (setter) and the provider of the bean to be given as the setter argument
	 * 
	 * @param <K>
	 * @param <V>
	 */
	private static class Relation<K, V> implements ResultSetRowAssembler<K> {
		
		private final BeanRelationFixer<K, V> relationFixer;
		
		private final CachingResultSetRowTransformer<V, ?> transformer;
		
		public Relation(BeanRelationFixer<K, V> relationFixer, CachingResultSetRowTransformer<V, ?> transformer) {
			this.relationFixer = relationFixer;
			this.transformer = transformer;
		}
		
		@Override
		public void assemble(K bean, ResultSet resultSet) {
			// getting the bean
			V value = transformer.transform(resultSet);
			// applying it to the setter
			if (value != null) {
				relationFixer.apply(bean, value);
			}
		}

		@Override
		public Relation<K, V> copyWithAliases(Function<String, String> columnMapping) {
			return new Relation<>(relationFixer, new CachingResultSetRowTransformer<>(transformer.transformer.copyWithAliases(columnMapping)));
		}
	}
	
	private static class Assembler<O> {
		
		private final ResultSetRowAssembler<O> resultSetRowAssembler;
		
		private final AssemblyPolicy policy;
		
		private Assembler(ResultSetRowAssembler<O> resultSetRowAssembler, AssemblyPolicy policy) {
			this.resultSetRowAssembler = resultSetRowAssembler;
			this.policy = policy;
		}
		
		public ResultSetRowAssembler<O> getResultSetRowAssembler() {
			return resultSetRowAssembler;
		}
		
		public AssemblyPolicy getPolicy() {
			return policy;
		}
		
		/**
		 * Implementation to avoid collision in Set, based on {@link ResultSetRowAssembler} only because we don't want to assemble beans twice
		 * because their relation differ in {@link AssemblyPolicy}, there one can't specify twice same combiner with different policies.
		 */
		@Override
		public boolean equals(Object o) {
			if (this == o) return true;
			if (o == null || getClass() != o.getClass()) return false;
			
			Assembler assembler = (Assembler) o;
			return resultSetRowAssembler.equals(assembler.resultSetRowAssembler);
		}
		
		/**
		 * Implementation to avoid collision in Set, based on {@link ResultSetRowAssembler} only because we don't want to assemble beans twice
		 * because their relation differ in {@link AssemblyPolicy}, there one can't specify twice same combiner with different policies.
		 */
		@Override
		public int hashCode() {
			return resultSetRowAssembler.hashCode();
		}
	}
	
	/**
	 * A small class that stores a relation between a root bean ad an assembler. It acts as a key in {@link HashSet} to mark a relation as treated
	 * to prevent the relation to be applied multiple times whereas it shouldn't, according to the {@link AssemblyPolicy#ONCE_PER_BEAN} strategy.
	 * 
	 * @param <K> root bean type
	 */
	private static class TreatedRelation<K> {
		
		private final K rootBean;
		
		private final ResultSetRowAssembler<K> assembler;
		
		private TreatedRelation(K rootBean, ResultSetRowAssembler<K> assembler) {
			this.rootBean = rootBean;
			this.assembler = assembler;
		}
		
		/** Implementation to avoid collision in Set */
		@Override
		public boolean equals(Object o) {
			if (this == o) return true;
			if (o == null || getClass() != o.getClass()) return false;
			
			TreatedRelation<?> that = (TreatedRelation<?>) o;
			
			if (!rootBean.equals(that.rootBean)) return false;
			return assembler.equals(that.assembler);
		}
		
		/** Implementation to avoid collision in Set */
		@Override
		public int hashCode() {
			int result = rootBean.hashCode();
			result = 31 * result + assembler.hashCode();
			return result;
		}
	}
	
	/**
	 * Policy introduced to specify if assembly shall be done for each row of a {@link ResultSet} (case of {@link java.util.Collection}
	 * to be filled for instance) or only once per root bean (simple setter case)
	 */
	public enum AssemblyPolicy {
		/** Specifies that assembly shall be done on each row of a {@link ResultSet} ({@link java.util.Collection} case) */
		ON_EACH_ROW,
		/** Specifies that assembly shall be done once (and only onnce) per root bean (setter case) */
		ONCE_PER_BEAN
	}
	
	private interface RootConverter<C, I> {
		
		<T extends C> ResultSetRowTransformer<T, I> copyFor(Class<T> beanType, SerializableFunction<I, T> beanFactory);
		
		<T extends C> ResultSetRowTransformer<T, I> copyFor(Class<T> beanType, SerializableSupplier<T> beanFactory);
		
		ResultSetRowTransformer<C, I> copyWithAliases(Function<String, String> columnMapping);
		
		C transform(ResultSet resultSet);
	}
	
	/**
	 * Cache system over a {@link ResultSetRowTransformer} that gives already created instance if bean key matches.
	 * Due to being based on bean key, this cache doesn't make sense with beans that don't have key (no-arg constructor),
	 * thus, is such case, a new instance will be created each time {@link #transform(ResultSet)} is called.
	 */
	private static class CachingResultSetRowTransformer<C, I> implements RootConverter<C, I> {
		
		private final ResultSetRowTransformer<C, I> transformer;
		
		private final Function<ResultSet, C> newInstanceProvider;
		
		private CachingResultSetRowTransformer(ResultSetRowTransformer<C, I> transformer) {
			this.transformer = transformer;
			// computing the instance creator according to the given bean factory
			BeanFactory<C> beanFactory = transformer.getBeanFactory();
			if (beanFactory instanceof ResultSetRowTransformer.NoIdentifierBeanFactory) {
				newInstanceProvider = rs -> ((NoIdentifierBeanFactory<C>) beanFactory).createInstance();
			} else if (beanFactory instanceof ResultSetRowTransformer.IdentifierArgBeanFactory) {
				newInstanceProvider = rs -> {
					I beanKey = ((IdentifierArgBeanFactory<I, C>) beanFactory).readBeanKey(rs);
					try {
						// we don't call the cache if the bean key is null
						return beanKey == null ? null : CURRENT_BEAN_CACHE.get().computeIfAbsent(transformer.getBeanType(), beanKey, i -> transformer.transform(rs));
					} catch (ClassCastException cce) {
						// Trying to give a more accurate reason that the default one
						// Put into places for rare cases of misusage by wrapping code that loses reader and bean factory types,
						// ending with a constructor input type error
						MethodReferenceCapturer methodReferenceCapturer = new MethodReferenceCapturer();
						throw new ClassCastException("Can't apply " + beanKey + " on constructor "
								+ Reflections.toString(methodReferenceCapturer.findExecutable(((IdentifierArgBeanFactory<I, C>) beanFactory).getFactory())) + " : " + cce.getMessage());
					}
				};
			} else {
				// should not happen
				throw new IllegalArgumentException("Class " + Reflections.toString(beanFactory.getClass()) + " is not implemented");
			}
		}
		
		public C transform(ResultSet resultSet) {
			return newInstanceProvider.apply(resultSet);
		}
		
		@Override
		public <T extends C> ResultSetRowTransformer<T, I> copyFor(Class<T> beanType, SerializableFunction<I, T> beanFactory) {
			return this.transformer.copyFor(beanType, beanFactory);
		}
		
		@Override
		public <T extends C> ResultSetRowTransformer<T, I> copyFor(Class<T> beanType, SerializableSupplier<T> beanFactory) {
			return this.transformer.copyFor(beanType, beanFactory);
		}
		
		@Override
		public ResultSetRowTransformer<C, I> copyWithAliases(Function<String, String> columnMapping) {
			return this.transformer.copyWithAliases(columnMapping);
		}
	}
}
